// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/okex/api_order/native_private_client.h"

#include <memory>
#include <string>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/okex/api_order/enums.h"
#include "coin2/exchange/okex/symbology/product.h"

using coin2::exchange::okex::api_order::OrderStatus;
using coin2::exchange::okex::symbology::OkexProduct;

template <typename Container>
std::string ToList(Container container) {
  std::string res;
  for (auto& v : container) {
    if (res.empty()) {
      res += v;
    } else {
      res += ",";
      res += v;
    }
  }
  return res;
}

auto OkexNativePrivateClient::GetRestOrderParams(const OrderSpec& order_spec, int64_t proc_order_id)
    -> RestParams {
  static std::string prefix("pl");
  auto params = RestParams();

  auto product = OkexProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_price(formatter.FormatPrice(order_spec.price));
  params.set_size(formatter.FormatQty(order_spec.qty));
  params.set_client_oid(prefix + std::to_string(proc_order_id));

  params.set_instrument_id(order_spec.product);

  if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_type("market");
  } else {
    params.set_type("limit");
  }
  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("buy");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("sell");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  if (order_spec.post_only) {
    params.set_order_type("1");
  } else if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
    params.set_order_type("2");
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    params.set_order_type("3");
  } else {
    params.set_order_type("0");
  }

  return params;
}

auto OkexNativePrivateClient::GetRestAmendParams(
    const OrderSpec& order_spec,
    const std::string& exchange_order_id,
    int64_t proc_order_id,
    int64_t orig_proc_order_id) -> RestParams {
  static std::string prefix("pl");
  auto params = RestParams();

  auto product = OkexProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_price(formatter.FormatPrice(order_spec.price));
  params.set_size(formatter.FormatQty(order_spec.qty));
  params.set_client_oid(prefix + std::to_string(proc_order_id));

  params.set_instrument_id(order_spec.product);

  if (!exchange_order_id.empty()) {
    params.set_order_id(exchange_order_id);
  }
  if (orig_proc_order_id > 0) {
    params.set_client_oid(prefix + std::to_string(orig_proc_order_id));
  }
  if (proc_order_id > 0) {
    params.set_request_id(std::to_string(proc_order_id));
  }

  // TODO(daniel): unsupport cancel on fail yet
  params.set_cancel_on_fail("0");

  return params;
}

auto OkexNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  const auto& order_spec = order_context.order_spec();
  OkexProduct product = OkexProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto params = RestParams();
  params.set_instrument_id(product.order_native_symbol());
  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id());
  } else {
    params.set_client_oid(std::to_string(order_context.proc_order_id()));
  }
  return params;
}

OkexNativePrivateClient::OkexNativePrivateClient(const NativeClientConfig& config_override) {
  NativeClientConfig config;
  config.set_rest_host("www.okex.com");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[OkexNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<OkexRestAuth, NativeClientConfig>(config);
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::QueryAccountInfo() {
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/spot/v3/accounts", &builder);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::QueryOrders(
    const std::string& instrument_id,
    int state) {
  UrlParamsBuilder builder;
  builder.PutGet("instrument_id", instrument_id);
  builder.PutGet("state", state);
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/spot/v3/orders", &builder);

  if (state == static_cast<int>(OrderStatus::OPEN)) {
    context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  } else if (state == static_cast<int>(OrderStatus::DONE)) {
    context->query_type = OrderSubTopicId::REST_DONE_ORDERS;
  } else {
    CHECK(false);
  }
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::QueryFills(
    const std::string& instrument_id,
    int limit) {
  UrlParamsBuilder builder;
  builder.PutGet("instrument_id", instrument_id);
  builder.PutGet("limit", limit);
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/spot/v3/fills", &builder);

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::PlaceOrder(const RestParams& params) {
  UrlParamsBuilder builder;
  builder.PutPost("instrument_id", params.instrument_id());
  builder.PutPost("side", params.side());
  if (params.has_type()) {
    builder.PutPost("type", params.type());
  }
  if (params.has_client_oid()) {
    builder.PutPost("client_oid", params.client_oid());
  }
  if (params.has_price()) {
    builder.PutPost("price", params.price());
  }
  if (params.has_size()) {
    builder.PutPost("size", params.size());
  }
  if (params.has_order_type()) {
    builder.PutPost("order_type", params.order_type());
  }
  if (params.has_notional()) {
    builder.PutPost("notional", params.notional());
  }
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/api/spot/v3/orders", &builder);

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::AmendOrder(const RestParams& params) {
  UrlParamsBuilder builder;
  builder.PutPost("instrument_id", params.instrument_id());
  builder.PutPost("cancel_on_fail", params.cancel_on_fail());
  if (params.has_order_id()) {
    builder.PutPost("order_id", params.order_id());
  } else if (params.has_client_oid()) {
    builder.PutPost("client_oid", params.client_oid());
  }
  if (params.has_request_id()) {
    builder.PutPost("request_id", params.request_id());
  }
  if (params.has_price()) {
    builder.PutPost("new_price", params.price());
  }
  if (params.has_size()) {
    builder.PutPost("new_size", params.size());
  }
  auto* context = GetAuth()->CreateRequestByPostWithSignature(
      "/api/spot/v3/amend_order/" + params.instrument_id(),
      &builder);

  context->query_type = OrderSubTopicId::AMEND_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::CancelSingleOrder(
    const RestParams& params) {
  UrlParamsBuilder builder;

  builder.PutPost("instrument_id", params.instrument_id());
  CHECK(params.has_order_id() != params.has_client_oid())
      << "only one of order_id and client_oid can be set!" << params.DebugString();

  if (params.has_order_id() > 0) {
    builder.PutPost("order_id", params.order_id());
    auto* context = GetAuth()->CreateRequestByPostWithSignature(
        "/api/spot/v3/cancel_orders/" + params.order_id(),
        &builder);

    context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  } else {
    builder.PutPost("client_oid", params.client_oid());
    auto* context = GetAuth()->CreateRequestByPostWithSignature(
        "/api/spot/v3/cancel_orders/" + params.client_oid(),
        &builder);

    context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  }
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::CancelOrders(const RestParams& params) {
  UrlParamsBuilder builder;

  builder.PutPost("instrument_id", params.instrument_id());
  CHECK((params.order_ids_size() > 0) != (params.client_oids_size() > 0))
      << "only one of order_id and client_order_id can be set!" << params.DebugString();

  if (params.order_ids_size() > 0) {
    builder.PutPost("order_id", ToList(params.order_ids()));
  } else {
    builder.PutPost("client_order_id", ToList(params.client_oids()));
  }

  auto* context =
      GetAuth()->CreateRequestByPostWithSignature("/api/spot/v3/cancel_batch_orders", &builder);

  context->query_type = OrderSubTopicId::MULTI_CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::AmendOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  const auto orig_proc_order_id = order_spec.orig_proc_order_id;
  const auto& exchange_order_id = order_exe.oc ? order_exe.oc->exchange_order_id() : "";
  auto params =
      GetRestAmendParams(order_spec, exchange_order_id, proc_order_id, orig_proc_order_id);
  return AmendOrder(params);
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelSingleOrder(params);
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::QueryTransfer(
    const std::string& ccy,
    const std::string& type) {
  UrlParamsBuilder builder;
  builder.PutGet("currency", ccy);
  builder.PutGet("type", type);
  builder.PutGet("limit", 100);
  std::string query_transfer_path = "/api/spot/v3/accounts/";
  if (ccy != "") {
    query_transfer_path += ccy + "/";
  }
  query_transfer_path += "ledger";
  auto* context = GetAuth()->CreateRequestByGetWithSignature(query_transfer_path, &builder);

  context->query_type = OrderSubTopicId::TRANSFER;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelSingleOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> OkexNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountInfo();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      return QueryOrders(params.instrument_id(), 0);
    }
    default: {
      return nullptr;
    }
  }
}
